using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using _Path = System.IO.Path;

#if BUILD_PEANUTBUTTER_INTERNAL
namespace Imported.PeanutButter.Utils
#else
namespace PeanutButter.Utils
#endif
{
    /// <summary>
    /// Provides a mechanism to create, read and write a temporary file which
    /// is automatically removed at disposal time
    /// </summary>
#if BUILD_PEANUTBUTTER_INTERNAL
    internal
#else
    public
#endif
        class AutoTempFile : IDisposable
    {
        /// <summary>
        /// How many times to retry operations if they fail
        /// - operations like read and write are retried on
        ///   failure as I've seen antivirus hold onto files ):
        /// </summary>
        public int RetryOperations
        {
            get => _retryOperations;
            set => _retryOperations = Math.Min(1, value);
        }

        private int _retryOperations = 50;

        /// <summary>
        /// Provides the path to the temporary file on disk
        /// </summary>
        public string Path => _tempFile;

        /// <summary>
        /// Reads and writes binary data from and to the file
        /// </summary>
        public byte[] BinaryData
        {
            get => Retry.Max(RetryOperations).Times(() => File.ReadAllBytes(_tempFile));
            set => Retry.Max(RetryOperations).Times(() => File.WriteAllBytes(_tempFile, value));
        }

        /// <summary>
        /// Reads and writes string data from and to the file
        /// </summary>
        public string StringData
        {
            get => Retry.Max(RetryOperations).Times(() => Encoding.UTF8.GetString(File.ReadAllBytes(_tempFile)));
            set => Retry.Max(RetryOperations).Times(
                () => File.WriteAllBytes(_tempFile, Encoding.UTF8.GetBytes(value ?? string.Empty))
            );
        }

        private string _tempFile;
        private AutoDeleter _actual;
        private readonly object _lock = new object();

        /// <summary>
        /// Default constructor: creates the temp file with no data and no naming hints
        /// using the OS-level temporary filename generator.
        /// </summary>
        public AutoTempFile() : this(_Path.GetTempPath(), null, null)
        {
        }

        /// <summary>
        /// Constructs a new AutoTempFile with the provided binary data. The file
        /// name is generated by the operating system.
        /// </summary>
        /// <param name="data"></param>
        public AutoTempFile(byte[] data) : this(_Path.GetTempPath(), null, data)
        {
        }

        /// <summary>
        /// Constructs a new AutoTempFile with the provided string data. The file
        /// name is generated by the operating system.
        /// </summary>
        /// <param name="data"></param>
        public AutoTempFile(string data) : this(_Path.GetTempPath(), null, Encoding.UTF8.GetBytes(data))
        {
        }

        /// <summary>
        /// Constructs a new AutoTempFile in the specified baseFolder with the provided string data
        /// </summary>
        /// <param name="baseFolder">Folder to create the file within</param>
        /// <param name="data">String data to initialize the file with</param>
        public AutoTempFile(string baseFolder, string data) : this(
            baseFolder,
            null,
            Encoding.UTF8.GetBytes(data ?? string.Empty)
        )
        {
        }

        /// <summary>
        /// Constructs a new AutoTempFile in the specified baseFolder with the provided binary data
        /// </summary>
        /// <param name="baseFolder">Folder to create the file within</param>
        /// <param name="data">Binary data to initialize the file with</param>
        public AutoTempFile(string baseFolder, byte[] data) : this(baseFolder, null, data)
        {
        }

        /// <summary>
        /// Constructs a new AutoTempFile in the provided folder with the provided
        /// name, containing the provided binary data
        /// </summary>
        /// <param name="baseFolder">Folder to create the file within</param>
        /// <param name="fileName">Name to use for the file</param>
        /// <param name="data">Binary data to initialize the file with</param>
        public AutoTempFile(string baseFolder, string fileName, byte[] data)
        {
            SetTempFileNameWith(baseFolder, fileName);
            _actual = new AutoDeleter(_tempFile);
            baseFolder = _Path.GetDirectoryName(_tempFile) ?? string.Empty;
            if (!Directory.Exists(baseFolder))
            {
                Retry.Max(RetryOperations).Times(() => Directory.CreateDirectory(baseFolder));
            }

            Retry.Max(RetryOperations).Times(
                () =>
                {
                    File.WriteAllBytes(
                        _tempFile,
                        data ?? new byte[]
                        {
                        }
                    );
                }
            );
        }

        private readonly List<Func<string, string, string>> _fileNameStrategies =
            new()
            {
                FileNameWhenFolderAndFileNotSpecified,
                FileNameWhenFolderSpecifiedAndFileNotSpecified,
                FileNameWhenFolderNotSpecifiedAndFileIsSpecified,
                _Path.Combine
            };

        private static string FileNameWhenFolderNotSpecifiedAndFileIsSpecified(string folder, string file)
        {
            return folder == null && file != null
                ? _Path.Combine(_Path.GetTempPath(), file)
                : null;
        }

        private static string FileNameWhenFolderSpecifiedAndFileNotSpecified(string folder, string file)
        {
            return folder != null && file == null
                ? GetNewFileNameUnder(folder)
                : null;
        }

        private static string GetNewFileNameUnder(string folder)
        {
            string fileName;
            do
            {
                var tempFile = GetFileNameOfNewTempFile();
                fileName = _Path.Combine(folder, tempFile);
            } while (File.Exists(fileName));

            return fileName;
        }

        private static string GetFileNameOfNewTempFile()
        {
            var tempFileName = _Path.GetTempFileName();
            Retry.Max(50).Times(() => File.Delete(tempFileName));
            return _Path.GetFileName(tempFileName);
        }

        private static string FileNameWhenFolderAndFileNotSpecified(string folder, string file)
        {
            return folder == null && file == null
                ? _Path.GetTempFileName()
                : null;
        }

        private void SetTempFileNameWith(string baseFolder, string fileName)
        {
            _tempFile = _fileNameStrategies
                .Select(strategy => strategy(baseFolder, fileName))
                .First(path => path != null);
        }

        /// <summary>
        /// Deletes the temp file, if found, otherwise
        /// does nothing
        /// </summary>
        public void Delete()
        {
            if (!File.Exists(_tempFile))
            {
                return;
            }

            try
            {
                File.Delete(_tempFile);
            }
            catch (Exception)
            {
                if (File.Exists(_tempFile))
                {
                    throw;
                }
            }
        }

        /// <inheritdoc />
        public void Dispose()
        {
            lock (_lock)
            {
                _actual?.Dispose();
                _actual = null;
            }
        }
    }
}